Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ

Node / meshtastic / Meshtastic-Android / files / feature / firmware / src / commonTest / kotlin / org / meshtastic / feature / firmware / ota / dfu / LegacyDfuRetryPolicyTest.kt

Displaying Raw β€’ Download

feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuRetryPolicyTest.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 30.84 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)

Tff7b72package T7ee787org.meshtastic.feature.firmware.ota.dfu

Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionState
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFailsWith
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertIs
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Tests the Legacy DFU retry policy extracted into [runDfuRetryLoop]. The retry loop is driven entirely by lambdas β€” a
* session-result supplier and a stale-reset cleanup hook β€” so the policy can be exercised without bringing up the full
* BLE stack. Covers the drop/stale/success sequences that the mid-stream-disconnect recovery design depends on.
*/
Tff7b72class T56d364LegacyDfuRetryPolicyTest Tb4b4b4{

T8b949e/**
* Sequence A: NORMAL upload drops mid-stream β†’ stale cleanup β†’ RECOVERY upload succeeds.
*
* Verifies:
* - only 1 upload attempt is consumed before success (stale cleanup does NOT consume an upload attempt)
* - RECOVERY profile is selected for the post-drop attempts
* - exactly one stale-reset cleanup cycle runs
* - the surfaced result is Success
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`drop then stale then success consumes one upload attempt and switches to RECOVERY`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
T8b949e// Confirmed-Legacy style budget: full budget before and after engagement.
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
Tff7b72when Tb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4) Tb4b4b4{
T79c0ff1 Tff7b72-Tff7b72>
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff2T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tff7b72falseTb4b4b4,
Tb4b4b4)

T79c0ff2 Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DfuUploadResultTb4b4b4.Te6edf3SuccessTb4b4b4, Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4, Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4)Tb4b4b4,
Te6edf3sessionsTb4b4b4,
Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3staleResetsTb4b4b4)
Tb4b4b4}

T8b949e/**
* Sequence B: drop β†’ stale β†’ RECOVERY drop β†’ stale β†’ RECOVERY success.
*
* Verifies the run fits within 3 upload attempts + 2 stale resets (the budget), each stale-reset cycle runs exactly
* once per drop, and the final RECOVERY attempt succeeds.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`drop stale drop stale success fits within 3 uploads and 2 stale resets`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
T8b949e// Alternating: attempts 1, 3 drop mid-stream; attempts 2, 4 are stale; attempt 5 succeeds.
Tff7b72when Tb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4) Tb4b4b4{
T79c0ff1Tb4b4b4,
T79c0ff3Tb4b4b4,
Tff7b72-Tff7b72>
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff2T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tff7b72falseTb4b4b4,
Tb4b4b4)

T79c0ff2Tb4b4b4,
T79c0ff4Tb4b4b4,
Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DfuUploadResultTb4b4b4.Te6edf3SuccessTb4b4b4, Te6edf3outcomeTb4b4b4)
T8b949e// 5 sessions total, all but the first are RECOVERY.
Te6edf3assertEqualsTb4b4b4(T79c0ff5Tb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3sessionsTb4b4b4.Te6edf3dropTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4, Te6edf3staleResetsTb4b4b4)
Tb4b4b4}

T8b949e/**
* Repeated stale responses must stop at MAX_LEGACY_STALE_RESETS even though none of them consume an upload attempt
* β€” otherwise a wedged bootloader would loop forever.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`repeated stale responses stop at MAX_LEGACY_STALE_RESETS`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3sessions Tff7b72= T79c0ff0
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3sessionsTff7b72+Tff7b72+
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4.Te6edf3errorTb4b4b4)
T8b949e// The loop tolerates MAX_LEGACY_STALE_RESETS stale responses that trigger cleanup; the next stale response
T8b949e// terminates the loop.
Te6edf3assertEqualsTb4b4b4(Te6edf3MAX_LEGACY_STALE_RESETS Tff7b72+ T79c0ff1Tb4b4b4, Te6edf3sessionsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4, Te6edf3staleResetsTb4b4b4)
Tb4b4b4}

T8b949e/**
* Ordinary failures (not MidStreamDisconnect, not StaleSessionReset) consume the upload-attempt budget. After
* `attempts` consecutive ordinary failures, the loop exits with the last failure.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`ordinary failures consume the three upload attempts`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3sessions Tff7b72= T79c0ff0
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3sessionsTff7b72+Tff7b72+
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffordinary failureTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72falseTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4.Te6edf3errorTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3sessionsTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3staleResetsTb4b4b4, Ta5d6ff"Ta5d6ffordinary failures must NOT trigger stale cleanupTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Every retry that is actually going to happen is announced (next attempt number, total) before the inter-attempt
* delay, and the exhausted last attempt is not β€” the UI must never promise a retry that will not come.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`retries are announced before the delay and not after the last attempt`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3announced Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3PairTff7b72<Tffa657IntTb4b4b4, Tffa657IntTff7b72>Tff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3order Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3orderTb4b4b4.Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffuploadTa5d6ff"Tb4b4b4)
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffordinary failureTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72falseTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{ Te6edf3orderTb4b4b4.Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffdelayTa5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3onRetryScheduled Tff7b72= Tb4b4b4{ Te6edf3nextTb4b4b4, Te6edf3total Tff7b72-Tff7b72>
Te6edf3orderTb4b4b4.Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffannounceTa5d6ff"Tb4b4b4)
Te6edf3announcedTb4b4b4.Te6edf3addTb4b4b4(Te6edf3next Te6edf3to Te6edf3totalTb4b4b4)
Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(T79c0ff2 Te6edf3to T79c0ff3Tb4b4b4, T79c0ff3 Te6edf3to T79c0ff3Tb4b4b4)Tb4b4b4, Te6edf3announcedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffuploadTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffannounceTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffdelayTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffuploadTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffannounceTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffdelayTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffuploadTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3orderTb4b4b4)
Tb4b4b4}

T8b949e/** A stale-session cleanup is not a retry of the upload and must not be announced as one. */
Tf0883e@Test
Tff7b72fun Td2a8ff`stale cleanup is not announced as a retry`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3announcements Tff7b72= T79c0ff0
Tff7b72var Te6edf3sessions Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3sessionsTff7b72+Tff7b72+
Tff7b72if Tb4b4b4(Te6edf3sessions Tff7b72=Tff7b72= T79c0ff1Tb4b4b4) Tb4b4b4{
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3onRetryScheduled Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3_ Tff7b72-Tff7b72> Te6edf3announcementsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DfuUploadResultTb4b4b4.Te6edf3SuccessTb4b4b4, Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3announcementsTb4b4b4)
Tb4b4b4}

T8b949e/**
* A StaleSessionReset must NOT consume an upload attempt β€” the cleanup cycle never tried to upload. After the stale
* cleanup, the same upload-attempt budget remains, and a subsequent successful attempt succeeds.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`stale cleanup does not consume an upload attempt`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3sessions Tff7b72= T79c0ff0
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3sessionsTff7b72+Tff7b72+
Tff7b72if Tb4b4b4(Te6edf3sessions Tff7b72=Tff7b72= T79c0ff1Tb4b4b4) Tb4b4b4{
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DfuUploadResultTb4b4b4.Te6edf3SuccessTb4b4b4, Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3sessionsTb4b4b4, Ta5d6ff"Ta5d6ffstale cleanup must not consume an upload attempt β€” 2 sessions for 1 successTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3staleResetsTb4b4b4)
Tb4b4b4}

T8b949e/**
* The profile switches from NORMAL to RECOVERY only after a MidStreamDisconnect. A StaleSessionReset by itself must
* NOT switch the profile β€” the link did not actually drop mid-upload.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`NORMAL switches to RECOVERY only after MidStreamDisconnect`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)

T8b949e// Initial stale session, then mid-stream drop, then success β€” only the post-drop attempt(s) should be RECOVERY.
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
Tff7b72when Tb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4) Tb4b4b4{
T79c0ff1 Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)

T79c0ff2 Tff7b72-Tff7b72>
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff2T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tff7b72falseTb4b4b4,
Tb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

T8b949e// Session 1 (stale) stayed NORMAL; session 2 (drop) was still NORMAL; session 3 (post-drop) switched to
T8b949e// RECOVERY.
Te6edf3assertEqualsTb4b4b4(
Te6edf3listOfTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4)Tb4b4b4,
Te6edf3sessionsTb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e/**
* An initial StaleSessionReset (no prior drop) keeps NORMAL β€” the most common case of a device stranded in the
* bootloader from a previous interrupted flash.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`initial StaleSessionReset keeps NORMAL profile`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)

Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
Tff7b72when Tb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4) Tb4b4b4{
T79c0ff1 Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tff7b72else Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3sessionsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4}

T8b949e/**
* The original mid-stream failure stays visible even when the terminal failure is a later cleanup/transfer error.
* The retry loop attaches the first MidStreamDisconnect as a suppressed exception on the surfaced error.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`original MidStreamDisconnect stays visible when terminal failure differs`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Tff7b72when Tb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{
Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMAL Tff7b72-Tff7b72>
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff5T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tff7b72falseTb4b4b4,
Tb4b4b4)

T8b949e// All RECOVERY attempts fail with ordinary errors until the budget is exhausted.
Tff7b72else Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffrecovery transfer failedTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72falseTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Tff7b72val Te6edf3terminal Tff7b72= Te6edf3outcomeTb4b4b4.Te6edf3error
Te6edf3assertIsTff7b72<Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3terminalTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3terminalTb4b4b4.Te6edf3suppressedExceptionsTb4b4b4.Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3suppressed Tff7b72= Te6edf3terminalTb4b4b4.Te6edf3suppressedExceptionsTb4b4b4.Te6edf3singleTb4b4b4(Tb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTff7b72>Tb4b4b4(Te6edf3suppressedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff5T79c0ff0Tb4b4b4, Te6edf3suppressedTb4b4b4.Te6edf3bytesSentTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2T79c0ff0T79c0ff0Tb4b4b4, Te6edf3suppressedTb4b4b4.Te6edf3totalBytesTb4b4b4)
Tb4b4b4}

T8b949e/**
* Secure DFU never throws the Legacy-only exceptions, so it just runs the `attempts` loop β€” no stale cleanup, no
* profile switching.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`Secure protocol runs the attempts loop without stale handling`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0

Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3SECURETb4b4b4,
T8b949e// Secure budget is always 1/1 β€” pre==engaged, so engagement promotion is a no-op.
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffsecure transfer failedTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72falseTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertIsTff7b72<Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4.Te6edf3errorTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4)
T8b949e// Secure never switches profile even if a MidStreamDisconnect-shaped failure surfaced (it cannot β€”
T8b949e// MidStreamDisconnect is a LegacyDfuException subtype).
Te6edf3sessionsTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3staleResetsTb4b4b4)
Tb4b4b4}

T8b949e/** Secure DFU succeeds as soon as the first session succeeds β€” no fallback, no profile switching. */
Tf0883e@Test
Tff7b72fun Td2a8ff`Secure protocol returns Success on first successful session`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3SECURETb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DfuUploadResultTb4b4b4.Te6edf3SuccessTb4b4b4, Te6edf3outcomeTb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Two-stage budget: pre-engagement probe β†’ engaged promotion
T8b949e// -----------------------------------------------------------------------

T8b949e/**
* Unknown Legacy pre-engagement connect failure: budget stays at the speculative 1-attempt cap so Secure fallback
* stays timely. The probe never engaged, so no promotion occurs.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`Unknown Legacy pre-engagement connect failure stops after 1 attempt for timely Secure fallback`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3sessions Tff7b72= T79c0ff0
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3sessionsTff7b72+Tff7b72+
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffconnect failedTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72falseTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3sessionsTb4b4b4, Ta5d6ff"Ta5d6ffpre-engagement budget must be 1 so Secure can fall back in timeTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Unknown Legacy engages then MidStreamDisconnect: budget promotes from 1 β†’ 3 inside the loop, and subsequent
* attempts use RECOVERY.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`Unknown Legacy engagement during MidStreamDisconnect promotes budget to 3 and uses RECOVERY`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3size Tff7b72=Tff7b72= T79c0ff1Tb4b4b4) Tb4b4b4{
T8b949e// First attempt: engages, then drops mid-stream.
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff5T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tff7b72trueTb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
T8b949e// Subsequent RECOVERY attempts fail ordinarily to exhaust the promoted budget.
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffrecovery failedTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffpromoted engaged budget must allow 3 attemptsTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3NORMALTb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3sessionsTb4b4b4.Te6edf3dropTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3assertEqualsTb4b4b4(Te6edf3LegacyDfuStreamProfileTb4b4b4.Te6edf3RECOVERYTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Unknown Legacy engages, drop β†’ stale β†’ success: the stale cleanup after engagement does NOT consume the promoted
* budget. Three upload sessions total (drop, stale, success) but only one upload-attempt budget slot consumed
* before success.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`Unknown Legacy stale cleanup after engagement does not consume promoted budget`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3staleResets Tff7b72= T79c0ff0
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
Tff7b72when Tb4b4b4(Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4) Tb4b4b4{
T79c0ff1 Tff7b72-Tff7b72>
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff5T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tff7b72trueTb4b4b4,
Tb4b4b4)

T79c0ff2 Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)

Tff7b72else Tff7b72-Tff7b72> Te6edf3DfuUploadResultTb4b4b4.Te6edf3Success
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{ Te6edf3staleResetsTff7b72+Tff7b72+ Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3DfuUploadResultTb4b4b4.Te6edf3SuccessTb4b4b4, Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffdrop+stale+success = 3 sessions; stale cleanup does not consume budgetTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3staleResetsTb4b4b4)
Tb4b4b4}

T8b949e/**
* A Legacy alternate that engages (e.g. SecureObserved primary fails after engagement β†’ Legacy fallback) receives
* the full Legacy engaged budget of 3.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`Legacy alternate that engages receives full engaged Legacy budget`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3sessions Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3LegacyDfuStreamProfileTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3LEGACYTb4b4b4,
T8b949e// Legacy alternate: pre=1 (probe), engaged=3 (full Legacy budget).
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LEGACY_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{ Te6edf3profile Tff7b72-Tff7b72>
Te6edf3sessionsTb4b4b4.Te6edf3addTb4b4b4(Te6edf3profileTb4b4b4)
T8b949e// Engages immediately, then fails until the budget is exhausted.
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6fflegacy alt failureTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff3Tb4b4b4, Te6edf3sessionsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffLegacy alternate that engages must receive full engaged budgetTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Secure never receives a Legacy-style engaged promotion: its budget carries pre==engaged==1, so even when a Secure
* session reports protocolEngaged=true the active attempt cap stays at 1.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`Secure budget stays at 1 even when Secure session reports protocolEngaged true`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72var Te6edf3sessions Tff7b72= T79c0ff0
Tff7b72val Te6edf3outcome Tff7b72=
Te6edf3runDfuRetryLoopTb4b4b4(
Te6edf3protocol Tff7b72= Te6edf3DfuProtocolKindTb4b4b4.Te6edf3SECURETb4b4b4,
Te6edf3budget Tff7b72= Te6edf3DfuAttemptBudgetTb4b4b4(Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4, Te6edf3LIMITED_SESSION_ATTEMPTSTb4b4b4)Tb4b4b4,
Te6edf3maxStaleResets Tff7b72= Te6edf3MAX_LEGACY_STALE_RESETSTb4b4b4,
Te6edf3runUploadSession Tff7b72= Tb4b4b4{
Te6edf3sessionsTff7b72+Tff7b72+
T8b949e// Even with protocolEngaged=true, the Secure budget cannot grow (pre==engaged==1).
Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTb4b4b4(Te6edf3DfuExceptionTb4b4b4.Te6edf3TransferFailedTb4b4b4(Ta5d6ff"Ta5d6ffsecure engages then failsTa5d6ff"Tb4b4b4)Tb4b4b4, Tff7b72trueTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3resetStaleBootloader Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3interAttemptDelay Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertIsTff7b72<Te6edf3DfuUploadResultTb4b4b4.Te6edf3FailureTff7b72>Tb4b4b4(Te6edf3outcomeTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3sessionsTb4b4b4, Ta5d6ff"Ta5d6ffSecure budget must stay at 1 even when protocolEngaged=true (no Legacy promotion)Ta5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e// -----------------------------------------------------------------------
T8b949e// Cleanup policy (cleanupDfuSessionTransport)
T8b949e// -----------------------------------------------------------------------

T8b949e/** Fake transport for cleanup-policy tests β€” records abort/close calls and can inject failures. */
Tff7b72private Tff7b72class T56d364FakeCleanupTransportTb4b4b4(Tff7b72val Te6edf3abortActionTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4, Tff7b72val Te6edf3closeActionTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4) Tb4b4b4:
Te6edf3DfuUploadTransport Tb4b4b4{
Tff7b72var Te6edf3abortCalled Tff7b72= Tff7b72false
Tff7b72var Te6edf3closeCalled Tff7b72= Tff7b72false

Tff7b72override Tff7b72val Te6edf3isLowSpeedTransferTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72false

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffconnectToDfuModeTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8fftransferInitPacketTb4b4b4(Te6edf3initPacketTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72= Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8fftransferFirmwareTb4b4b4(Te6edf3firmwareTb4b4b4: Te6edf3ByteArrayTb4b4b4, Te6edf3onProgressTb4b4b4: Te6edf3suspend Tb4b4b4(Tffa657FloatTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4)Tb4b4b4: Te6edf3ResultTff7b72<Tffa657UnitTff7b72> Tff7b72=
Te6edf3ResultTb4b4b4.Te6edf3successTb4b4b4(Tffa657UnitTb4b4b4)

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffabortTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3abortCalled Tff7b72= Tff7b72true
Te6edf3abortActionTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3closeCalled Tff7b72= Tff7b72true
Te6edf3closeActionTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`cleanup closes transport after abort throws Exception`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// Even if abort throws an operational Exception, close must still be called. The exception propagates after
T8b949e// close because the helper's finally guarantees close.
Tff7b72val Te6edf3transport Tff7b72= Te6edf3FakeCleanupTransportTb4b4b4(Te6edf3abortAction Tff7b72= Tb4b4b4{ Tff7b72throw Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffabort failedTa5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3assertFailsWithTff7b72<Te6edf3RuntimeExceptionTff7b72> Tb4b4b4{
Te6edf3cleanupDfuSessionTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3completed Tff7b72= Tff7b72falseTb4b4b4, Te6edf3sessionFailure Tff7b72= Tff7b72nullTb4b4b4)
Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3abortCalledTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3closeCalledTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`cleanup closes transport after abort throws Error`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// Error subtypes propagate through the cleanup helper's finally β€” close still runs.
Tff7b72val Te6edf3transport Tff7b72= Te6edf3FakeCleanupTransportTb4b4b4(Te6edf3abortAction Tff7b72= Tb4b4b4{ Tff7b72throw Te6edf3AssertionErrorTb4b4b4(Ta5d6ff"Ta5d6ffabort errorTa5d6ff"Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3assertFailsWithTff7b72<Te6edf3AssertionErrorTff7b72> Tb4b4b4{
Te6edf3cleanupDfuSessionTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3completed Tff7b72= Tff7b72falseTb4b4b4, Te6edf3sessionFailure Tff7b72= Tff7b72nullTb4b4b4)
Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3abortCalledTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3closeCalledTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`cleanup skips abort after StaleSessionReset but always closes`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72= Te6edf3FakeCleanupTransportTb4b4b4(Tb4b4b4)
Te6edf3cleanupDfuSessionTransportTb4b4b4(
Te6edf3transportTb4b4b4,
Te6edf3completed Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3sessionFailure Tff7b72= Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3StaleSessionResetTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3abortCalledTb4b4b4, Ta5d6ff"Ta5d6ffabort must be skipped after StaleSessionResetTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffclose must always runTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`cleanup skips abort after MidStreamDisconnect but always closes`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72= Te6edf3FakeCleanupTransportTb4b4b4(Tb4b4b4)
Te6edf3cleanupDfuSessionTransportTb4b4b4(
Te6edf3transportTb4b4b4,
Te6edf3completed Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3sessionFailure Tff7b72=
Te6edf3LegacyDfuExceptionTb4b4b4.Te6edf3MidStreamDisconnectTb4b4b4(
T79c0ff2T79c0ff0Tb4b4b4,
T79c0ff2T79c0ff0T79c0ff0Tb4b4b4,
Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3lastConfirmedBytes Tff7b72= Tff7b72-T79c0ff1Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3abortCalledTb4b4b4, Ta5d6ff"Ta5d6ffabort must be skipped after MidStreamDisconnectTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffclose must always runTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`cleanup attempts abort after ambiguous ConnectionFailed and always closes`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72= Te6edf3FakeCleanupTransportTb4b4b4(Tb4b4b4)
Te6edf3cleanupDfuSessionTransportTb4b4b4(
Te6edf3transportTb4b4b4,
Te6edf3completed Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3sessionFailure Tff7b72= Te6edf3DfuExceptionTb4b4b4.Te6edf3ConnectionFailedTb4b4b4(Ta5d6ff"Ta5d6ffconnect failedTa5d6ff"Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3abortCalledTb4b4b4, Ta5d6ff"Ta5d6ffabort must be attempted after ConnectionFailed (link may still be usable)Ta5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffclose must always runTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`cleanup skips abort after completed session but always closes`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3transport Tff7b72= Te6edf3FakeCleanupTransportTb4b4b4(Tb4b4b4)
Te6edf3cleanupDfuSessionTransportTb4b4b4(Te6edf3transportTb4b4b4, Te6edf3completed Tff7b72= Tff7b72trueTb4b4b4, Te6edf3sessionFailure Tff7b72= Tff7b72nullTb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3abortCalledTb4b4b4, Ta5d6ff"Ta5d6ffabort must be skipped after a completed sessionTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3transportTb4b4b4.Te6edf3closeCalledTb4b4b4, Ta5d6ff"Ta5d6ffclose must always runTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.14s